#include <sys/select.h>
#include <sys/types.h>
#include <cctype>
#include <cerrno>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <functional>
/// --- config space

#define APP_VERSION "0.0.1"

#define DEFAULT_HOST "irc.oftc.net"
#define DEFAULT_PORT "6667"
#define TIMESTAMP_FORMAT "%Y-%m-%d %R"
#define COMMAND_PREFIX_CHARACTER ':'
#define DEFAULT_PARTING_MESSAGE "luvsic - 250 LOC are too much!"
/// --- end config space

/// --- data space
char *argv0;
static char *host = DEFAULT_HOST;
static char *port = DEFAULT_PORT;
static char *password;
static char nickname[32];
static char bufin[4096];
static char bufout[4096];
static char channel[256];
static time_t trespond;
static FILE *srv;
/// --- end data space

/// --- macro space

/* use main(int argc, char *argv[]) */
#define ARGBEGIN                                     \
	for (argv0 = *argv, argv++, argc--;              \
		 argv[0] && argv[0][1] && argv[0][0] == '-'; \
		 argc--, argv++)                             \
	{                                                \
		char argc_;                                  \
		char **argv_;                                \
		int brk_;                                    \
		if (argv[0][1] == '-' && argv[0][2] == '\0') \
		{                                            \
			argv++;                                  \
			argc--;                                  \
			break;                                   \
		}                                            \
		for (brk_ = 0, argv[0]++, argv_ = argv;      \
			 argv[0][0] && !brk_;                    \
			 argv[0]++)                              \
		{                                            \
			if (argv_ != argv)                       \
				break;                               \
			argc_ = argv[0][0];                      \
			switch (argc_)

/* Handles obsolete -NUM syntax */
#define ARGNUM \
	case '0':  \
	case '1':  \
	case '2':  \
	case '3':  \
	case '4':  \
	case '5':  \
	case '6':  \
	case '7':  \
	case '8':  \
	case '9'

#define ARGEND \
	}          \
	}

#define ARGC() argc_

#define ARGNUMF(base) (brk_ = 1, estrtol(argv[0], (base)))

#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL) ? ((x), abort(), (char *)0) : (brk_ = 1, (argv[0][1] != '\0') ? (&argv[0][1]) : (argc--, argv++, argv[0])))

#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL) ? (char *)0 : (brk_ = 1, (argv[0][1] != '\0') ? (&argv[0][1]) : (argc--, argv++, argv[0])))
/// --- end macro space

/// --- func space
#undef strlcpy
size_t strlcpy(char *dst, const char *src, size_t sz)
{
	char *d = dst;
	const char *s = src;
	size_t len = sz;
	if (len != 0)
	{
		while (--len != 0)
		{
			if ((*d++ = *s++) == '\0')
				break;
		}
	}

	if (len == 0)
	{
		if (sz != 0)
			*d = '\0';

		while (*s++)
			;
	}

	return (s - src - 1);
}

static void eprint(const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	vsnprintf(bufout, sizeof(bufout), fmt, ap);
	va_end(ap);

	fprintf(stderr, "%s", bufout);
	if (fmt[0] && fmt[strlen(fmt) - 1] == ':')
		fprintf(stderr, " %s\n", strerror(errno));
	exit(1);
}

static int dial(char *host, char *port)
{
	addrinfo hints;
	addrinfo *res, *r;
	int fd;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

	if (getaddrinfo(host, port, &hints, &res) != 0)
		eprint("[ERROR]: connot resolve hostname '%s':", host);

	for (r = res; r; r = r->ai_next)
	{
		// socket not set ok
		if ((fd = socket(r->ai_family, r->ai_socktype, r->ai_protocol)) == -1)
			continue;

		if (connect(fd, r->ai_addr, r->ai_addrlen) == 0)
			break;

		close(fd);
	}

	freeaddrinfo(res);

	if (!r)
		eprint("[ERROR]: cannot connect to host '%s'\n", host);

	return fd;
}

static char *eat(char *s, std::function<int(int)> fn, int r)
{
	while (*s != '\0' && fn((unsigned char)*s) == r)
	{
		s++;
	}

	return s;
}

static char *skip(char *s, char c)
{
	while (*s != c && *s != '\0')
	{
		s++;
	}
	if (*s != '\0')
		*s++ = '\0';
	return s;
}

static void
trim(char *s)
{
	char *e;
	for (e = s + strlen(s); e > s && isspace((unsigned char)*(e - 1)); e--)
		;
	*e = '\0';
}

static void
pipeout(char *channel, char *fmt, ...)
{
	static char timestr[80];
	time_t t;
	va_list ap;
	va_start(ap, fmt);
	vsnprintf(bufout, sizeof(bufout), fmt, ap);
	va_end(ap);

	t = time(nullptr);

	strftime(timestr, sizeof(timestr), TIMESTAMP_FORMAT, localtime(&t));

	fprintf(stdout, "%-12s: %s %s\n", channel, timestr, bufout);
}

static void streamout(char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	vsnprintf(bufout, sizeof(bufout), fmt, ap);
	va_end(ap);
	fprintf(srv, "%s\r\n", bufout);
}

static void privmsg(char *channel, char *msg)
{
	if (channel[0] == '\0')
	{
		pipeout("", "No channel to send to");
		return;
	}

	pipeout(channel, "<%s> %s", nickname, msg);
	streamout("PRIVATE MESSAGE %s: %s", channel, msg);
}

static void
parsein(char *s)
{
	char c;
	char *p;
	if (s[0] == '\0')
		return;
	skip(s, '\n');
	if (s[0] != COMMAND_PREFIX_CHARACTER)
	{
		privmsg(channel, s);
		return;
	}
	c = *++s;
	if (c != '\0' && isspace((unsigned char)s[1]))
	{
		p = s + 2;
		switch (c)
		{
		case 'j':
			streamout("JOIN %s", p);
			if (channel[0] == '\0')
				strlcpy(channel, p, sizeof(channel));
			return;
		case 'l':
			s = eat(p, isspace, 1);
			p = eat(s, isspace, 0);
			if (!*s)
				s = channel;
			if (*p)
				*p++ = '\0';
			if (!*p)
				p = DEFAULT_PARTING_MESSAGE;
			streamout("PART %s :%s", s, p);
			return;
		case 'm':
			s = eat(p, isspace, 1);
			p = eat(s, isspace, 0);
			if (*p)
				*p++ = '\0';
			privmsg(s, p);
			return;
		case 's':
			strlcpy(channel, p, sizeof(channel));
			return;
		default:
			eprint("[ERROR]: seems touch unknown or unsupported parsing in data");
			return;
		}
	}
	streamout("%s", s);
}

static void parsesrv(char *cmd)
{
	char *usr, *par, *txt;
	usr = host;
	if (!cmd || !*cmd)
		return;
	if (cmd[0] == ':')
	{
		usr = cmd + 1;
		cmd = skip(usr, ' ');
		if (cmd[0] == '\0')
			return;
		skip(usr, '!');
	}

	skip(cmd, '\r');
	par = skip(cmd, ' ');
	txt = skip(par, ':');
	trim(par);

	if (!strcmp("PONG", cmd))
		return;
	if (!strcmp("PRIVMSG", cmd))
		pipeout(par, "<%s> %s", usr, txt);
	else if (!strcmp("PING", cmd))
		streamout("PONG %s", txt);
	else
	{
		pipeout(usr, "| $> %s (%s): %s", cmd, par, txt);
		if (!strcmp("NICK", cmd) && !strcmp(usr, nickname))
			strlcpy(nickname, txt, sizeof(nickname));
	}
}
static void
usage(void)
{
	eprint("usage: luvsic [-h host] [-p port] [-n nick] [-k keyword] [-v]\n", argv0);
}
/// --- end func space

int main(int argc, char *argv[])
{
	timeval tv;
	const char *username = getenv("USER");
	int n;
	fd_set rd;

	strlcpy(nickname, username ? username : "Unknown nickname", sizeof(nickname));
	if (argc == 1)
	{
		std::printf("[INFO]: start program with no args, continue now, see usage using arg -h\n");

		ARGBEGIN
		{
		case 'h':
			host = EARGF(usage());
			break;
		case 'p':
			port = EARGF(usage());
			break;
		case 'n':
			strlcpy(nickname, EARGF(usage()), sizeof(nickname));
			break;
		case 'k':
			password = EARGF(usage());
			break;
		case 'v':
			eprint("luvsic-" APP_VERSION " fucker fucker\n");
			break;
		default:
			usage();
		}
		ARGEND;
	}

	srv = fdopen(dial(host, port), "r+");
	if (!srv)
		eprint("fdopen:");

	if (password)
		streamout("PASSWORD %s", password);
	streamout("NICKNAME %s", nickname);
	streamout("USER %s localhost %s :%s", nickname, host, nickname);
	fflush(srv);
	setbuf(stdout, NULL);
	setbuf(srv, NULL);
	setbuf(stdin, NULL);

	for (;;)
	{
		FD_ZERO(&rd);
		FD_SET(0, &rd);
		FD_SET(fileno(srv), &rd);
		tv.tv_sec = 120;
		tv.tv_usec = 0;
		n = select(fileno(srv) + 1, &rd, 0, 0, &tv);
		if (n < 0)
		{
			if (errno == EINTR)
				continue;
			eprint("luvsic: error no select():");
		}
		else if (n == 0)
		{
			if (time(nullptr) - trespond >= 300)
				eprint("luvsic shutting down: parse timeout\n");
			streamout("PING %s", host);
			continue;
		}

		if (FD_ISSET(fileno(srv), &rd))
		{
			if (fgets(bufin, sizeof(bufin), srv) == NULL)
				eprint("luvsic: remote host closed connection\n");
			parsesrv(bufin);
			trespond = time(nullptr);
		}

		if (FD_ISSET(0, &rd))
		{
			if (fgets(bufin, sizeof(bufin), stdin) == nullptr)
				eprint("luv sic: broken pipe\n");
			parsein(bufin);
		}
	}

	return 0;
}
